/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using VSLangProj80;
using System.IO;
using EnvDTE;
using VSLangProj;
using System.Windows.Forms;
using DaveSexton.DocProject.Engine;
using System.Threading;
using DaveSexton.DocProject.Sandcastle.Resources;
using System.Xml;
using System.Xml.XPath;

namespace DaveSexton.DocProject.Sandcastle
{
	/// <summary>
	/// Builds documentation for a given DocProject using the Sandcastle utility.
	/// </summary>
	/// <remarks>
	/// This class is not thread-safe.
	/// </remarks>
	public class SandcastleBuildEngine : BuildEngine<SandcastleSettings, SandcastleProjectOptions>
	{
		#region Public Properties
		public override HelpSystem HelpSystem
		{
			get
			{
				string documentationSetFileName = Settings.DocumentationSetFileName;

				return new SandcastleHelpSystem(Settings.RelativeTargetDirectory,
					Path.Combine(Settings.TargetDirectory, documentationSetFileName + ".hhc"),
					Path.Combine(Settings.TargetDirectory, documentationSetFileName + ".hhk"));
			}
		}

		public override MissingBuildTypeDependency DocSiteContentsMissingBuildType
		{
			get
			{
				return MissingBuildTypeDependency.Partial;
			}
		}

		public override MissingBuildTypeDependency DocSiteIndexMissingBuildType
		{
			get
			{
				return MissingBuildTypeDependency.Partial;
			}
		}

		public override Type BuildProcessComponentInitializerType
		{
			get
			{
				return typeof(BPCInitializer);
			}
		}

		public override DateTime LastBuild
		{
			get
			{
				DateTime lastBuild = base.LastBuild;

				if (lastBuild == DateTime.MinValue)
					lastBuild = new SystemPath(Settings.TargetDirectory, "Html", false, true).Info.LastWriteTime;
				
				return lastBuild;
			}
		}
		#endregion

		#region Private / Protected
		protected override DocProjectItemCollection BuildItemsInternal
		{
			get
			{
				if (!SandcastleBuildEngineProvider.VerifySandcastleInstalled())
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Errors.SandcastleNotInstalled, Environment.NewLine));

				/* NOTE:
				 * Items paths must be absolute.
				 * The order in which items are added is the order in which a user is prompted to include their files as 
				 * project items (only applies to items that are configured to be included in the project).
				 */
				DocProjectItemCollection items = new DocProjectItemCollection();

				string target = Settings.TargetDirectory;
				string configPath = Settings.RelativeConfigurationPath ?? Settings.ConfigurationPath;

				if (!Path.IsPathRooted(configPath))
					configPath = Path.Combine(target, configPath);

				bool produceHelp1x = Options.Help1xProject.OutputEnabled;
				bool produceHelp2x = Options.Help2xProject.OutputEnabled;

				// Items with a MissingBuildTypeDependency value of Error should be listed first, although it's not required, 
				// so that none of the other items are checked for updates if the item is missing.

				if (produceHelp1x || !produceHelp2x)		// default .config file if no compiled help is built
				{
					items.Add(new DocProjectFile(Path.Combine(configPath, Options.Help1xConfigFileName), false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
					items.Add(new DocProjectFile(Path.Combine(configPath, Options.Help1xConceptualConfigFileName), false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
				}

				if (produceHelp2x && !Settings.UseHelp1HtmlToCompileHelp2)
				{
					items.Add(new DocProjectFile(Path.Combine(configPath, Options.Help2xConfigFileName), false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
					items.Add(new DocProjectFile(Path.Combine(configPath, Options.Help2xConceptualConfigFileName), false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
				}

				items.Add(new DocProjectFile(Path.Combine(configPath, Options.MRefBuilderConfigurationFileName), false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));

				if (produceHelp1x)
					items.Add(new DocProjectFile(Settings.Help1xProjectConfigFile, false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Partial));

				if (produceHelp2x)
					items.Add(new DocProjectFile(Settings.Help2xProjectConfigFile, false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Partial));

				if (Settings.BuildConceptualTopics)
				{
					IEnumerable<string> additionalFiles = TopicManagement.TopicsDocument.GetAllFiles(Settings.TopicsFile);
					items.Add(new DocProjectFileGroup(Settings.TopicsDirectory, additionalFiles, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));

					items.Add(new DocProjectFile(Settings.ConceptualArtMapFile, false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
					items.Add(new DocProjectFile(Settings.ConceptualSnippetsFile, false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));
				}

				items.Add(new DocProjectFile(Settings.BibliographyFile, false, false, MissingBuildTypeDependency.Error, UpdatedBuildTypeDependency.Full));

				// ^^ Items with a MissingBuildTypeDependency value of Error should be added first (although it's not required) ^^

				items.Add(new DocProjectFile(Settings.TokensFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				items.Add(new DocProjectFile(Settings.GlobalMetadataFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				items.Add(new DocProjectFile(Settings.LinksFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));

				if (Settings.BuildConceptualTopics)
				{
					items.Add(new DocProjectFile(Settings.ConceptualProjectMetadataFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
					items.Add(new DocProjectFile(Settings.ConceptualVersionMetadataFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				}

				items.Add(new DocProjectFileGroup(SystemPath.Recursive(Settings.TopicsHtmlDirectory, "*.htm", true), false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Partial));

				items.Add(new DocProjectFile(Settings.TopicsFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				items.Add(new DocProjectFile(Settings.ProjectXmlFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				items.Add(new DocProjectFile(Settings.DynamicFiltersFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));
				items.Add(new DocProjectFile(Settings.VersionsFile, false, false, MissingBuildTypeDependency.None, UpdatedBuildTypeDependency.Full));

				// NOTE: The missing build type dependency used to be Partial for the following two items, but since they are target output (SandcastleSettings.Targets) it no longer matters.
				// This is because the new build item tracking feature simply compares the last write time of each build item with that of the target output instead of caching
				// build item write times, as was done previously.  So if the target outputs are missing, there is no last write time to which the build items can be compared
				// and as a result, a full build is always executed.  One way around this would be to support some type of build item dependency feature so that it could be determined 
				// whether or not all required items exist in the case of a missing target, instead of comparing file write times to DateTime.MinValue, but it doesn't seem worth the effort.
				if (produceHelp1x)
					items.Add(new DocProjectFile(Settings.CompiledHelp1xFile, true, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.None, true));

				if (produceHelp2x)
					items.Add(new DocProjectFile(Settings.CompiledHelp2xFile, true, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.None, true));

				if (produceHelp1x || Settings.HtmlHelpWorkshopFilesRequired || !produceHelp2x)
					items.Add(new DocProjectFolder(Path.Combine(target, "Html"), false, false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.None));

				if (produceHelp2x && !Settings.UseHelp1HtmlToCompileHelp2)
					items.Add(new DocProjectFolder(Path.Combine(target, "Html2"), false, false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.None));

				string documentationSetFileName = Settings.DocumentationSetFileName;

				if (produceHelp1x)
				{
					items.Add(new DocProjectFile(Path.Combine(target, documentationSetFileName + ".hhp"), false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Path.Combine(target, documentationSetFileName + ".hhc"), false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Path.Combine(target, documentationSetFileName + ".hhk"), false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.Partial));
				}

				if (produceHelp2x)
				{
					items.Add(new DocProjectFile(Path.Combine(target, documentationSetFileName + ".HxC"), false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Path.Combine(target, documentationSetFileName + ".HxT"), false, true, MissingBuildTypeDependency.Full, UpdatedBuildTypeDependency.Partial));

					items.Add(new DocProjectFile(Settings.Help2xFilesCollectionFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xAssociativeIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xDynamicLinkIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xSearchWindowIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xKeywordIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xNamedUrlIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
					items.Add(new DocProjectFile(Settings.Help2xContextSensitiveIndexFile, false, true, MissingBuildTypeDependency.Partial, UpdatedBuildTypeDependency.Partial));
				}

				return items;
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "SandcastleToolsPath cannot be marked as static because it's accessed from another AppDomain (SandcatsleBuildEngine is MarshalByRefObject).")]
		private string SandcastleToolsPath
		{
			get
			{
				return SandcastleBuildEngineProvider.Paths.Tools;
			}
		}

		private readonly SynchronizationContext syncContext = SynchronizationContext.Current;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildEngine" /> class for the specified <paramref name="project" />, which 
		/// must be a project created from a DocProject Template.
		/// </summary>
		public SandcastleBuildEngine(IDocProject project)
			: base(project)
		{
		}
		#endregion

		#region Methods
		private static XmlNode EnsureXmlDocumentationMembers(XmlDocument document)
		{
			XmlNode members = document.SelectSingleNode("//members");

			if (members == null)
			{
				XmlElement doc = document.DocumentElement;

				if (doc == null)
				{
					doc = document.CreateElement("doc");
					document.AppendChild(doc);
				}

				members = document.CreateElement("members");
				doc.AppendChild(members);
			}

			return members;
		}

		protected override void OnBuildStarting(BuildEventArgs e)
		{
			Options.CancelAsynchronousBuilds();

			base.OnBuildStarting(e);
		}

		public bool BuildToc(BuildContext context, bool forHelpBuild, bool mustExcludeRootTopic, bool tryRebuildTocXmlOnly, bool buildConceptualManifest, out string tocFile)
		{
			EnsureProjectSubdirectory(Settings.RelativeWorkingDirectory);

			try
			{
				context.TraceLine("Preparing to build the TOC...");

				tocFile = AddTocGenerationSteps(context.Steps, forHelpBuild, mustExcludeRootTopic, tryRebuildTocXmlOnly, Settings.BuildReferenceTopics, buildConceptualManifest);

				context.Sync = syncContext;

				context.TraceLine("Starting to build the TOC...");

				using (BuildStepExecutionEngine engine = new BuildStepExecutionEngine(context,
					delegate(IBuildStep step, BuildContext innerContext)
					{
						ExecuteStep(step, innerContext);
					}))
				{
					engine.ExecuteAll();
				}

				bool success = context.BuildState != BuildState.Failed && context.SuccessfullyExecutedAllRequiredSteps;

				context.TraceLine("TOC generation {0}.", (success) ? "completed successfully" : "failed");

				return success;
			}
			finally
			{
				foreach (IBuildStep step in context.Steps)
					step.Dispose();
			}
		}

		protected override void PrepareTargetDirectory(BuildContext context)
		{
			if (context.BuildType == BuildType.Full)
			{
				RemoveTargetFile(Settings.RelativeCompiledHelp1xFile);
				RemoveTargetFile(Settings.RelativeCompiledHelp2xFile);

				PrepareWorkingDirectory(true);
			}
		}

		private void PrepareWorkingDirectory(bool fullClean)
		{
			if (fullClean)
			{
				CleanProjectSubdirectory(Settings.RelativeWorkingDirectory);

				EnsureProjectSubdirectory(Path.Combine(Settings.BuildAssemblerRelativeWorkingDirectory, "Intellisense"));

				string commentsFolder = "Comments";
				EnsureProjectSubdirectory(Path.Combine(Settings.BuildAssemblerRelativeWorkingDirectory, commentsFolder));

				string projectXmlFile = Settings.ProjectXmlFile;
				string projectXmlFileTarget = Path.Combine(Settings.WorkingXmlDocumentationPath, Path.GetFileName(projectXmlFile));

				if (File.Exists(projectXmlFile))
				{
					File.Copy(projectXmlFile, projectXmlFileTarget, true);

					FileInfo file = new FileInfo(projectXmlFileTarget);
					file.IsReadOnly = false;
				}

				// Build Assember output will be copied to these directories after the build has completed: 
				DeleteTargetSubdirectory("Html");		// help 1.x
				DeleteTargetSubdirectory("Html2");	// help 2.x
			}
			else
			{
				CleanProjectSubdirectory(Path.Combine(Settings.BuildAssemblerRelativeWorkingDirectory, "Intellisense"));
				CleanProjectSubdirectory(Settings.BuildAssemblerRelativeOutputDirectory);
			}

			EnsureProjectSubdirectory(Path.Combine(Settings.BuildAssemblerRelativeOutputDirectory, "media"));
			EnsureProjectSubdirectory(Path.Combine(Settings.BuildAssemblerRelativeOutputDirectory, "html"));
		}

		protected override void PrepareSourceForBuild(SystemPath source, IList<SystemPath> xmlDocumentationFiles, BuildContext context)
		{
			if (context.BuildType == BuildType.Full)
			{
				string sourceName = source.FileName ?? source.Directory.Path;
				
				context.TraceLine("Merging XML documentation for {0}...", sourceName);
				
				bool keepSource = Options.MergeXmlDocumentation == MergeXmlDocumentation.KeepSource;

				List<string> mergedFiles = new List<string>(xmlDocumentationFiles.Count);

				Predicate<SystemPath> mergeDocumentation = delegate(SystemPath input)
				{
					string doc = null;

					if (xmlDocumentationFiles.Count == 0)
						return false;
					else if (xmlDocumentationFiles.Count > 1)
					// More than one file indicates that a folder or wildcard was specified as the current source's XML documentation.
					// Since the XML documentation files have already been imported by the base build engine, just check 
					// for a file that matches by name to merge with the external XML documentation.
					{
						string file = Path.Combine(Settings.WorkingXmlDocumentationPath, input.FileNameWithoutExtension + ".xml");

						if (File.Exists(file))
							doc = file;
					}
					else
						doc = xmlDocumentationFiles[0].Path;

					if (!string.IsNullOrEmpty(doc))
					{
						if (!mergedFiles.Exists(f => string.Equals(f, doc, StringComparison.OrdinalIgnoreCase)))
						{
							mergedFiles.Add(doc);

							MergeXmlDocumentationFile(keepSource, input, doc, context);
							return true;
						}
					}
					
					return false;
				};

				if (source.IsDirectory)
				{
					PrepareSources(mergeDocumentation, source.GetFiles("*.dll"));
					PrepareSources(mergeDocumentation, source.GetFiles("*.exe"));
					PrepareSources(mergeDocumentation, source.GetFiles("*.org"));
				}
				else
					PrepareSources(mergeDocumentation, source.GetFiles());
			}
		}

		private void PrepareSources(Predicate<SystemPath> mergeDocumentation, IEnumerable<SystemPath> files)
		{
			foreach (SystemPath source in files)
			{
				if (!mergeDocumentation(source))
					ImportXmlDocumentationFile(source);
			}
		}

		[System.Diagnostics.DebuggerHidden]
		private void ImportXmlDocumentationFile(SystemPath source)
		{
			string xmlDocumentationFile = source.FileNameWithoutExtension + ".xml";

			string input = Path.Combine(Settings.ExternalXmlDocumentationPath, xmlDocumentationFile);
			string target = Path.Combine(Settings.WorkingXmlDocumentationPath, xmlDocumentationFile);

			FileInfo file = new FileInfo(target);

			if (file.Exists)
				file.IsReadOnly = false;

			try
			{
				File.Copy(input, target, true);
			}
			catch (DirectoryNotFoundException) { }
			catch (FileNotFoundException) { }

			if (file.Exists)
				file.IsReadOnly = false;
		}

		private void MergeXmlDocumentationFile(bool keepSource, SystemPath source, string xmlDocumentationFile, BuildContext context)
		{
			XmlDocument document = new XmlDocument();

			try
			{
				document.PreserveWhitespace = true;
				document.Load(xmlDocumentationFile);

				string internalAssemblyName = null;
				XmlNode node = document.SelectSingleNode("doc/assembly/name");

				if (node != null)
					internalAssemblyName = node.InnerText.Trim();

				string externalDoc = GetExternalXmlDocumentationFile(internalAssemblyName, source, xmlDocumentationFile);

				if (externalDoc != null)
				{
					XmlNode targetMembers = EnsureXmlDocumentationMembers(document);

					XmlReaderSettings settings = new XmlReaderSettings();
					settings.IgnoreComments = true;
					settings.IgnoreProcessingInstructions = true;
					settings.IgnoreWhitespace = true;

					using (XmlReader reader = XmlReader.Create(externalDoc, settings))
					{
						if (reader.ReadToFollowing("members"))
						{
							using (XmlReader membersTree = reader.ReadSubtree())
							{
								MergeXmlDocumentationMembers(keepSource, source, xmlDocumentationFile, membersTree, targetMembers);
							}
						}
					}
				}

				document.Save(xmlDocumentationFile);
			}
			catch (IOException ex)
			{
				string message = string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Text.LoadXmlDocumentationIoException, xmlDocumentationFile, Environment.NewLine, ex.GetType().FullName, ex.Message);

				context.AddWarningToList("Sandcastle - Merge Xml Documentation", message, true);
			}
			catch (XmlException ex)
			{
				string message = string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Text.LoadXmlDocumentationXmlException, xmlDocumentationFile, Environment.NewLine, ex.GetType().FullName, ex.Message);

				context.AddWarningToList("Sandcastle - Merge Xml Documentation", message, true);
			}
		}

		private string GetExternalXmlDocumentationFile(string internalAssemblyName, SystemPath source, string xmlDocumentationFile)
		{
			string file = null;

			if (!string.IsNullOrEmpty(internalAssemblyName))
				file = Path.Combine(Settings.ExternalXmlDocumentationPath, internalAssemblyName) + ".xml";

			if (file != null && File.Exists(file))
				return file;
			else
			{
				file = Path.Combine(Settings.ExternalXmlDocumentationPath, source.FileName) + ".xml";

				if (File.Exists(file))
					return file;
				else if (!string.IsNullOrEmpty(xmlDocumentationFile))
				{
					file = Path.Combine(Settings.ExternalXmlDocumentationPath, Path.GetFileName(xmlDocumentationFile)) + ".xml";

					if (File.Exists(file))
						return file;
				}
			}

			return null;
		}

		private void MergeXmlDocumentationMembers(bool keepSource, SystemPath source, string xmlDocumentationFile, XmlReader members, XmlNode targetMembers)
		{
			members.ReadStartElement();

			if (members.NodeType == XmlNodeType.Element)
			{
				do
				{
					string apiName = members["name"];

					if (apiName != null)
					{
						XmlNode targetMember = targetMembers.SelectSingleNode("member[@name = '" + apiName + "']");

						if (targetMember == null)
						{
							XmlAttribute attribute = targetMembers.OwnerDocument.CreateAttribute("name");
							attribute.Value = apiName;

							targetMember = targetMembers.OwnerDocument.CreateElement("member");
							targetMember.Attributes.Append(attribute);

							targetMembers.AppendChild(targetMember);
						}

						using (XmlReader sectionsTree = members.ReadSubtree())
						{
							MergeXmlDocumentationSections(keepSource, source, xmlDocumentationFile, sectionsTree, targetMember);
						}
					}
				}
				while (members.ReadToNextSibling("member"));
			}
		}

		private void MergeXmlDocumentationSections(bool keepSource, SystemPath source, string xmlDocumentationFile, XmlReader sections, XmlNode targetMember)
		{
			sections.ReadStartElement();

			while (sections.NodeType == XmlNodeType.Element)
			{
				string sectionName = sections.Name;

				Dictionary<string, string> attributes = new Dictionary<string, string>(sections.AttributeCount);

				if (sections.HasAttributes)
				{
					while (sections.MoveToNextAttribute())
						attributes.Add(sections.LocalName, sections.Value);

					sections.MoveToElement();
				}

				string comments = null;

				if (sections.IsEmptyElement)
					sections.Skip();
				else
					comments = sections.ReadInnerXml();

				XmlNode targetSection = targetMember.SelectSingleNode(sectionName);

				if (targetSection == null)
				{
					targetSection = targetMember.OwnerDocument.CreateElement(sectionName);
					targetMember.AppendChild(targetSection);
				}
				else if (keepSource)
					continue;

				MergeXmlDocumentationSection(source.Path, xmlDocumentationFile, attributes, comments, targetSection);
			}
		}

		protected virtual void MergeXmlDocumentationSection(string assemblyOrFile, string xmlDocumentationFile,
			IDictionary<string, string> sourceAttributes, string sourceXmlSection, IXPathNavigable targetSection)
		{
			XPathNavigator navigator = targetSection.CreateNavigator();

			foreach (KeyValuePair<string, string> attribute in sourceAttributes)
			{
				if (navigator.MoveToAttribute(attribute.Key, ""))
				{
					navigator.SetValue(attribute.Value);
					navigator.MoveToParent();
				}
				else
					navigator.CreateAttribute(null, attribute.Key, null, attribute.Value);
			}

			if (sourceXmlSection != null)
				navigator.InnerXml = sourceXmlSection;
		}

		protected override void CleanInternal(BuildContext context)
		{
			base.CleanInternal(context);

			CleanProjectSubdirectory(Settings.RelativeWorkingDirectory);
		}

		protected override bool HasSources(BuildContext context)
		{
			return Settings.BuildReferenceTopics || Settings.BuildConceptualTopics;
		}

		protected override BuildType ValidateBuildType(BuildContext context, BuildType type)
		{
			if (type == BuildType.Partial)
			{
				if (Options.Help1xProject.OutputEnabled)
				{
					string htmlCompilerInputDirectory = Path.Combine(Settings.TargetDirectory, "Html");

					CopyDirectoryBuildStep<IBuildEngine> copyHtmlTopicsStep = new CopyDirectoryBuildStep<IBuildEngine>(context.Engine, Settings.TopicsHtmlDirectory, htmlCompilerInputDirectory, "*.htm");
					copyHtmlTopicsStep.IncludeInPartialBuild = true;
					copyHtmlTopicsStep.RunInBackground = true;
					copyHtmlTopicsStep.Copier.FlattenHierarchy = true;
					copyHtmlTopicsStep.Copier.SourceDirectoryRequired = false;

					context.Steps.Insert(context.Steps.IndexOf(HtmlHelpWorkshopStep.DefaultName), copyHtmlTopicsStep);
				}

				if (Options.Help2xProject.OutputEnabled)
				{
					string html2CompilerInputDirectory = Path.Combine(Settings.TargetDirectory, "Html2");

					CopyDirectoryBuildStep<IBuildEngine> copyHtmlTopicsStep = new CopyDirectoryBuildStep<IBuildEngine>(context.Engine, Settings.TopicsHtmlDirectory, html2CompilerInputDirectory, "*.htm");
					copyHtmlTopicsStep.IncludeInPartialBuild = true;
					copyHtmlTopicsStep.RunInBackground = true;
					copyHtmlTopicsStep.Copier.FlattenHierarchy = true;
					copyHtmlTopicsStep.Copier.SourceDirectoryRequired = false;

					context.Steps.Insert(context.Steps.IndexOf(Help2CompilerStep.DefaultName), copyHtmlTopicsStep);
				}
			}

			return type;
		}

		protected override BuildStepCollection CreateSteps()
		{
			if (!SandcastleBuildEngineProvider.VerifySandcastleInstalled())
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Errors.SandcastleNotInstalled, Environment.NewLine));

			if (!BuildAssemblerStep.BuildAssemblerApiLoaded)
				throw new InvalidOperationException(Errors.SandcastleBuildAssemblerNotFound);

			bool produceHelp1x = Options.Help1xProject.OutputEnabled;
			bool produceHelp2x = Options.Help2xProject.OutputEnabled;

			if (produceHelp2x && !Help2CompilerStep.Help2APILoaded)
				throw new InvalidOperationException(Errors.Help2CompilerNotFound);

			BuildStepCollection steps = new BuildStepCollection(this);

			if (SandcastleBuildEngineProvider.Configuration.GenerateFrameworkReflectionData)
				AddGenerateFrameworkReflectionSteps(steps);

			bool buildReference = Settings.BuildReferenceTopics;
			bool buildConceptual = Settings.BuildConceptualTopics;

			string tocFile = AddTocGenerationSteps(steps, true, false, false, buildReference, buildConceptual);

			if (buildConceptual)
				steps.Add(new CreateConceptualBuildFilesBuildStep(this, Settings.TopicsFile, 
					Settings.ConceptualBuildManifestFile, Settings.TopicsDirectory, Settings.WorkingConceptualDirectory, Settings.WorkingConceptualMetadataDirectory));

			bool workshopRequired = !produceHelp1x && Settings.HtmlHelpWorkshopFilesRequired;

			if (produceHelp1x || workshopRequired || !produceHelp2x)
				// Build Help 1.x if configured or default to Help 1.x if no compiled help is configured to be built.
				// If the project is a web application then some workshop files are required even if compiled help is not.
				AddHelp1Steps(steps, tocFile, 
					(buildReference) ? Settings.Help1ConfigurationFile : null, 
					(buildConceptual) ? Settings.Help1ConceptualConfigurationFile : null,
					produceHelp1x, workshopRequired);

			if (produceHelp2x)
			{
				bool useHtmlDirectory = Settings.UseHelp1HtmlToCompileHelp2;

				if ((produceHelp1x || workshopRequired) && !useHtmlDirectory)
					// clean-up after the build assembler so that it can run again to build 2.x help
					steps.Add(new PrepareWorkingDirectoryStep(this));

				AddHelp2Steps(steps, tocFile, 
					(buildReference) ? Settings.Help2ConfigurationFile : null,
					(buildConceptual) ? Settings.Help2ConceptualConfigurationFile : null, 
					useHtmlDirectory);
			}

			return steps;
		}

		private void AddGenerateFrameworkReflectionSteps(BuildStepCollection steps)
		{
			string root = SandcastleBuildEngineProvider.Paths.ReflectionDataRoot;
			string data = Settings.Presentation.ReflectionDataDirectory;

			if (!Directory.Exists(root))
			{
				Directory.CreateDirectory(root);

				if (string.IsNullOrEmpty(data))
				{
					steps.Add(new GenerateFrameworkReflectionDataBuildStep(this, root, data));
					return;
				}
			}

			string path = Path.Combine(root, data);

			if (!Directory.Exists(path))
			{
				Directory.CreateDirectory(path);

				steps.Add(new GenerateFrameworkReflectionDataBuildStep(this, path, data));
			}
		}

		internal string AddTocGenerationSteps(BuildStepCollection steps, bool forHelpBuild, bool mustExcludeRootTopic, bool tryRebuildTocXmlOnly, bool buildReference, bool buildConceptual)
		{
			string tocFile = Settings.TocFile;

			steps.AddChangeCurrentDirectory(Settings.WorkingDirectory);

			if (buildReference)
				buildReference = AddReferenceTocGenerationSteps(steps, forHelpBuild, mustExcludeRootTopic, !buildConceptual, tryRebuildTocXmlOnly);
			
			if (buildConceptual)
			{
				if (buildReference)
				{
					string merged = Settings.MergedTocFile;

					steps.Add(new MergeTocBuildStep(this, Settings.TopicsFile, tocFile, merged));

					tocFile = merged;
				}
				else
					// Process topics.xml file to remove nodes that have expose="False", although there's no reference TOC to merge with.
					steps.Add(new MergeTocBuildStep(this, Settings.TopicsFile, tocFile));
			}

			return tocFile;
		}

		private bool AddReferenceTocGenerationSteps(BuildStepCollection steps, bool forHelpBuild, bool mustExcludeRootTopic, bool required, bool tryRebuildTocXmlOnly)
		{
			string targetReflectionFile = Settings.ReflectionXmlFile;

			if (tryRebuildTocXmlOnly && File.Exists(targetReflectionFile))
				goto BuildTocXml;

			if (!forHelpBuild || !AddVersionBuilderSteps(steps))
			{
				List<string> assemblies = new List<string>(16), reflectionFiles = new List<string>(16);

				SandcastleSettings.AddSourcesInto(assemblies, true, reflectionFiles, false, Project.AllSources, s => s.Input);

				if (assemblies.Count == 0 && reflectionFiles.Count == 0)
				{
					if (!required || !forHelpBuild)
						return false;
					else
						throw new InvalidOperationException(Errors.NoSources);
				}

				if (assemblies.Count > 0)
				{
					steps.AddExternalProcess(Settings.MRefBuilder, (forHelpBuild) ? Settings.MRefBuilderConfigSwitch : string.Empty,
						"/out:" + SandcastleSettings.DefaultRawReflectionFileName, Settings.ScopeSwitch, Settings.CreateDependencySwitch(assemblies),
						string.Join(" ", assemblies.ToArray())).RunInBackground = true;
				}

				if (reflectionFiles.Count > 0)
					// Merge the previously output reflection.org with the reflection files provided by the user as external sources
					// to create one master reflection file (replaces reflection.org)
					steps.Add(new MergeReflectionFilesBuildStep(this, SandcastleSettings.DefaultRawReflectionFileName, reflectionFiles));
			}

			AddDocModelStep(steps, false, SandcastleSettings.DefaultRawReflectionFileName, Settings.ReflectionXmlFileName,
				(Options.UseFriendlyHtmlFileNames) ? Settings.Presentation.FriendlyFileNamesTransformation : Settings.Presentation.GuidFileNamesTransformation,
				(mustExcludeRootTopic) ? false : Options.GenerateRootApiTopic);

			if (forHelpBuild)
			{
				targetReflectionFile = Settings.TargetReflectionXmlFile;

				if (File.Exists(Settings.DynamicFiltersFile))
					// NOTE: Filters must be applied after the DocModel has been applied above because
					// topic filters are generated in the Topic Management dialog based on text that 
					// is retrieved from the reflection.xml file, with the DocModel already applied.
					steps.Add(new DynamicFiltersBuildStep(this, Settings.ReflectionXmlFile, Settings.TargetReflectionXmlFile, Settings.DynamicFiltersFile));
				else
					steps.AddCopyFile(Settings.ReflectionXmlFile, targetReflectionFile);
			}

			steps.AddExternalProcess(Settings.XslTransform,
				Settings.CreateXslTransformSwitch(SandcastleSettings.ReflectionToManifestTransformationFileName),
				"\"" + targetReflectionFile + "\"", "/out:\"" + Settings.ReferenceBuildManifestFileName + "\"");

		BuildTocXml:
			steps.AddExternalProcess(Settings.XslTransform,
				Settings.CreateXslTransformSwitch(Settings.Presentation.XmlTocTransformation),
				"\"" + targetReflectionFile + "\"",
				@"/out:""" + Settings.TocFileName + "\"");

			return true;
		}

		private bool AddVersionBuilderSteps(BuildStepCollection steps)
		{
			SourceVersionCollection versions = Options.Versions;
			IDictionary<string, string> versionNames = Options.SourceVersionNames;

			if (versions.Count == 0 && versionNames.Count == 0)
				return false;
			else
			{
				steps.Add(new VersionBuildStep(this, Project.AllSources, versions, versionNames));
				return true;
			}
		}

		internal void AddDocModelStep(BuildStepCollection steps, bool continueOnError, string input, string output, string fileNameTransformation, bool generateRootTopic)
		{
			List<string> xslTransformDocModelArgs = new List<string>();

			xslTransformDocModelArgs.Add(Settings.CreateXslTransformSwitch(Settings.Presentation.XmlDocModelTransformation));
			xslTransformDocModelArgs.Add("\"" + input + "\"");

			if (generateRootTopic)
				xslTransformDocModelArgs.Add("/arg:project=\"Project\"");		// "Project" must be specified instead of the actual project name because it's referenced that way throughout DocProject

			xslTransformDocModelArgs.Add(Settings.CreateXslTransformSwitch(fileNameTransformation));
			xslTransformDocModelArgs.Add("/out:\"" + output + "\"");
			xslTransformDocModelArgs.AddRange(Settings.CreateXslTransformArgumentSwitches(Settings.Presentation.XmlDocModelTransformationArgs));

			steps.AddExternalProcess(continueOnError, Settings.XslTransform, xslTransformDocModelArgs.ToArray()).RunInBackground = true;
		}

		private void AddHelp2Steps(BuildStepCollection steps, string tocFile, string referenceConfigFile, string conceptualConfigFile, bool compileHelp1Output)
		{
			string documentationSetFileName = Settings.DocumentationSetFileName;

			string projectTOC = Path.Combine(Settings.TargetDirectory, documentationSetFileName + ".HxT");
			string help2Project = Path.Combine(Settings.TargetDirectory, documentationSetFileName + ".HxC");

			steps.AddExternalProcess(Settings.XslTransform,
				SandcastleSettings.CreateXslTransformSwitch(Settings.ProjectConfigurationDirectory, Settings.Help2xProjectConfigFileName),
				"\"" + Settings.TargetReflectionXmlFile + "\"", 
				@"/arg:""basefilename=" + documentationSetFileName + "\"", 
				@"/arg:""output=" + Path.GetFileNameWithoutExtension(Settings.CompiledHelp2xFile) + "\"", 
				@"/arg:""langid=" + Settings.Lcid + "\"", 
				@"/out:""" + help2Project + "\"").IncludeInPartialBuild = true;

			steps.Add(new ImportHelpV2IndexAndFileList(this));

			steps.AddExternalProcess(Settings.XslTransform,
				Settings.CreateXslTransformSwitch(SandcastleSettings.TocToHxSContentsTransformationFileName),
				"\"" + tocFile + "\"",
				@"/out:""" + projectTOC + "\"").RunInBackground = true;

			if (!compileHelp1Output)
			{
				BuildAssemblerOptions options = Options.BuildAssemblerOptions;

				if (conceptualConfigFile != null)
					steps.Add(new BuildAssemblerStep(this, Settings.ConceptualBuildManifestFile, conceptualConfigFile, options));

				if (referenceConfigFile != null)
					steps.Add(new BuildAssemblerStep(this, Settings.ReferenceBuildManifestFile, referenceConfigFile, options));
			}

			string htmlCompilerInputDirectory = Path.Combine(Settings.TargetDirectory, "Html2");

			if (compileHelp1Output)
				// copy the html directory created when help 1.x was built to the Html2 directory referenced
				// by the files.HxF file.  The new directory is the source of .htm input to the help 2.x compiler.
				steps.AddCopyDirectory(Path.Combine(Settings.TargetDirectory, "Html"), htmlCompilerInputDirectory).RunInBackground = true;
			else
				// The transformations executed before this step require the html directory to be Output\html, so 
				// this step must occur after them.  There are no xml parameters or configuration options in 
				// Sandcastle to change the path from Output\html at this time.
				steps.AddMoveDirectory(Path.Combine(Settings.BuildAssemblerOutputDirectory, "html"), htmlCompilerInputDirectory).RunInBackground = true;

			// NOTE: This step is always added since it must be included in partial builds, even if the help 1.x output is being used for help 2.x and these files were already copied.
			CopyDirectoryBuildStep<IBuildEngine> copyHtmlTopicsStep = steps.AddCopyDirectory(Settings.TopicsHtmlDirectory, htmlCompilerInputDirectory, "*.htm");
			copyHtmlTopicsStep.RunInBackground = true;
			copyHtmlTopicsStep.Copier.FlattenHierarchy = true;
			copyHtmlTopicsStep.Copier.SourceDirectoryRequired = false;

			steps.Add(new UpdateHelp2TocFile(this, projectTOC));

			steps.Add(new Help2CompilerStep(this, help2Project));
		}

		private void AddHelp1Steps(BuildStepCollection steps, string tocFile, string referenceConfigFile, string conceptualConfigFile, bool compile, bool workshopRequired)
		{
			string documentationSetFileName = Settings.DocumentationSetFileName;
			string workshopProject = documentationSetFileName + ".hhp";
			string buildAssemblerOutputHtmlPath = Path.Combine(Settings.BuildAssemblerOutputDirectory, "html");
			string htmlCompilerInputDirectory = Path.Combine(Settings.RelativeTargetDirectory, "Html");

			BuildAssemblerOptions buildAssemblerOptions = Options.BuildAssemblerOptions;

			if (conceptualConfigFile != null)
				steps.Add(new BuildAssemblerStep(this, Settings.ConceptualBuildManifestFile, conceptualConfigFile, buildAssemblerOptions));

			if (referenceConfigFile != null)
				steps.Add(new BuildAssemblerStep(this, Settings.ReferenceBuildManifestFile, referenceConfigFile, buildAssemblerOptions));

			CopyDirectoryBuildStep<IBuildEngine> copyHtmlTopicsStep = steps.AddCopyDirectory(Settings.TopicsHtmlDirectory, buildAssemblerOutputHtmlPath, "*.htm");
			copyHtmlTopicsStep.RunInBackground = true;
			copyHtmlTopicsStep.Copier.FlattenHierarchy = true;
			copyHtmlTopicsStep.Copier.SourceDirectoryRequired = false;
			
			if (compile || workshopRequired)
			{
				steps.AddChangeCurrentDirectory(Project.Directory);

				// Produce the Help 1.x project, TOC, index files and prepare the HTML topics for localization.
				// (This step does not actually compile help.)
				steps.AddExternalProcess(Settings.ChmBuilderProgram,
					"/html:\"" + buildAssemblerOutputHtmlPath + "\"",
					"/toc:\"" + tocFile + "\"",
					"/project:\"" + documentationSetFileName + "\"",
					"/lcid:" + Settings.Lcid,
					Settings.ChmBuilderMetadataSwitch,
					// NOTE: this program requires a relative output path and automatically creates a folder named, "Html"
					"/out:" + Settings.RelativeTargetDirectory,
					"/config:\"" + Settings.Help1xProjectConfigFile + "\"").RunInBackground = true;
			}

			if (compile)
			{
				IBuildStep dbcsFix = steps.AddExternalProcess(Settings.DbcsFixProgram,
					// NOTE: this program requires a relative input path with no trailing slash
					"/d:\"" + htmlCompilerInputDirectory + "\"", 
					"/l:" + Settings.Lcid);
				dbcsFix.RunInBackground = true;
				dbcsFix.IncludeInPartialBuild = true;

				// TODO: Localization based on user settings (also requires updates to sandcastle.config and Help 2.x project file)
				// Start the HtmlHelpWorkshopStep using the AppLocale utility (requires XP, Server 2003 or Vista)
				// Example: Settings.AppLocaleProgram + " /L" + Settings.Lcid + " hhc.exe [hhc options]"	(no space or character after /L)
				steps.Add(new HtmlHelpWorkshopStep(this, Settings, Path.Combine(Settings.TargetDirectory, workshopProject)));
			}
		}
		#endregion

		#region Nested
		private sealed class BPCInitializer : BuildProcessComponentInitializer
		{
			#region Public Properties
			#endregion

			#region Private / Protected
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="BPCInitializer" /> class.
			/// </summary>
			public BPCInitializer()
			{
			}
			#endregion

			#region Methods
			public override void Initialize(BuildContext context, BuildProcessComponent component)
			{
				if (context == null)
					throw new ArgumentNullException("context");

				SandcastleBuildEngine engine = (SandcastleBuildEngine) context.Engine;

				// NOTE: SandcastleBuildEngineProvider.Paths.Tools cannot be used because it's static and hasn't been initialized 
				// since this code is running in another AppDomain.
				string path = engine.SandcastleToolsPath;

				BuildAssemblerLoader.TryLoadAssembly(path);
				BuildComponentsLoader.TryLoadAssembly(path);
			}
			#endregion
		}

		private sealed class ImportHelpV2IndexAndFileList : BuildStep<SandcastleBuildEngine>
		{
			public override string Name
			{
				get
				{
					return "Import missing Help 2.x files";
				}
			}

			public ImportHelpV2IndexAndFileList(SandcastleBuildEngine engine)
				: base(engine)
			{
				IncludeInPartialBuild = true;
			}

			protected override void ExecuteInternal(BuildContext context)
			{
				SandcastleSettings settings = Engine.Settings;

				bool importedAny = false;

				importedAny = ImportFile(settings.Help2xFilesCollectionFile, MSHelpV2.files, context);
				importedAny = importedAny | ImportFile(settings.Help2xAssociativeIndexFile, MSHelpV2.index_a, context);
				importedAny = importedAny | ImportFile(settings.Help2xDynamicLinkIndexFile, MSHelpV2.index_b, context);
				importedAny = importedAny | ImportFile(settings.Help2xContextSensitiveIndexFile, MSHelpV2.index_f, context);
				importedAny = importedAny | ImportFile(settings.Help2xKeywordIndexFile, MSHelpV2.index_k, context);
				importedAny = importedAny | ImportFile(settings.Help2xNamedUrlIndexFile, MSHelpV2.index_n, context);
				importedAny = importedAny | ImportFile(settings.Help2xSearchWindowIndexFile, MSHelpV2.index_s, context);

				if (!importedAny)
					context.TraceLine(Text.AllFilesFound);
				else
					context.TraceLine(Text.Done);
			}

			private static bool ImportFile(string file, string contents, BuildContext context)
			{
				if (!File.Exists(file))
				{
					context.TraceLine("Importing {0}", Path.GetFileName(file));

					context.Engine.Project.GetItem(file).Checkout();

					File.WriteAllText(file, contents, Encoding.UTF8);
					return true;
				}

				return false;
			}

			protected override void CancelInternal(BuildContext context)
			{
				// do nothing
			}
		}

		private sealed class PrepareWorkingDirectoryStep : BuildStep<SandcastleBuildEngine>
		{
			public PrepareWorkingDirectoryStep(SandcastleBuildEngine engine)
				: base(engine)
			{
			}

			public override string Name
			{
				get { return "Prepare Working Directory"; }
			}

			protected override void ExecuteInternal(BuildContext context)
			{
				Engine.PrepareWorkingDirectory(false);
			}

			protected override void CancelInternal(BuildContext context)
			{
				// do nothing
			}
		}

		private sealed class UpdateHelp2TocFile : BuildStep<SandcastleBuildEngine>
		{
			private readonly string toc;

			public UpdateHelp2TocFile(SandcastleBuildEngine engine, string toc)
				: base(engine)
			{
				this.toc = toc;
			}

			public override string Name
			{
				get { return "Updating Help 2.x TOC"; }
			}

			protected override void ExecuteInternal(BuildContext context)
			{
				string contents = File.ReadAllText(toc);

				contents = System.Text.RegularExpressions.Regex.Replace(contents, @"=""html\\", @"=""Html2\",
					System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled
					| System.Text.RegularExpressions.RegexOptions.CultureInvariant);

				context.Engine.Project.GetItem(toc).Checkout();

				File.WriteAllText(toc, contents);
			}

			protected override void CancelInternal(BuildContext context)
			{
				// do nothing
			}
		}
		#endregion
	}
}
